---
title: "Create or update a stored secret reference for the signed-in user."
method: PUT
path: "/v1/secrets/{scope}/{name}"
tags: ["secrets"]
---

# Create or update a stored secret reference for the signed-in user.

`PUT /v1/secrets/{scope}/{name}`

## Request

- Base URL: `https://api.kody.codes`
- URL: `https://api.kody.codes/v1/secrets/{scope}/{name}`
- Auth: HTTP bearer

Create or update a stored secret reference for the signed-in user. Use this for API keys, PATs, webhook HMAC secrets, and other static credentials already available inside trusted execution. Do not persist OAuth access or refresh tokens here — `/connect/oauth` and `createAuthenticatedFetch` / `integrationTokenRefresh` write those on the connection. Optional expires_at is a UTC ISO timestamp or YYYY-MM-DD; null clears expiry. Updates that only change description or expiry may omit value. Use `/connect/secret-set` for user-provided API key, PAT, and credential entry or rotation. Host use is authorized through secret policy approvals. Saved secrets are consumed in outbound `fetch` calls by placeholder, e.g. `{{secret:name}}`, resolved only for approved hosts.

Org permission: `secret:write`.

## Path parameters

- `scope` 'session' | 'package' | 'user', required: Storage scope that owns the secret.
- `name` string, required: Secret name to create or update.

## Request body

- object
  - `value` string: Secret value to persist. Required when creating a secret. Optional on updates that only change description or expires_at. Write-only and must never be returned to the caller.
  - `description` string: Optional human-readable description of the secret.
  - `expires_at` string, nullable: Optional UTC ISO expiry (or YYYY-MM-DD). Null or empty clears expiry. Omit on update to leave the existing expiry unchanged.
  - `kind` 'secret' | 'url' | 'login' | 'signing': Optional credential kind (ADR 0068), set together with a value. "secret" (default) is a Key used by `{{secret:name}}` placeholders. "url", "login", and "signing" are opt-in kinds a package reads through a scoped credential client, never as raw text; they need `hosts`.
  - `hosts` string[]: Hosts an opt-in kind may be sent to: exact hosts or one wildcard level such as *.example.com. Set together with a value. Keys keep using host approvals instead.
  - `login_exchange` object: For kind "login": how the login fields are exchanged for a session token. Its url must be on one of `hosts`.
    - `url` string, required
    - `method` 'POST' | 'PUT'
    - `bodyFormat` 'json' | 'form', required
    - `body` object, required
    - `tokenPath` string, required
    - `expiresInPath` string
    - `ttlSeconds` integer
  - `provider` string, nullable: Optional provider slug (for example "github") a scoped credential client matches package needs against. Null clears it. Keys used by placeholders ignore it.
  - `inject` union: Optional inject for a scoped credential client: a header with a "{value}" format, basic auth, or a query parameter. Defaults to `Authorization: Bearer {value}`. Placeholders ignore it.
    - object
      - `kind` 'header', required
      - `name` string, required
      - `format` string, required
    - object
      - `kind` 'basic', required
      - `valueAs` 'username' | 'password', required
      - `other` string
    - object
      - `kind` 'query', required
      - `param` string, required
  - `placement` object, nullable: Optional fill placement for placeholders (secret, secret-basic, and related forms): which request parts may receive the credential — headers (optionally named), query, path, and/or body. Null means anywhere. Distinct from inject, which only steers the scoped credential() client. The gateway does not enforce this yet.
    - `headers` union
      - true
      - string[]
    - `query` true
    - `path` true
    - `body` true

## Response `200`

Success.

- object
  - `name` string, required
  - `scope` 'session' | 'package' | 'user', required
  - `description` string, required
  - `package_id` string, nullable, required
  - `allowed_hosts` string[], required
  - `allowed_packages` string[], required
  - `created_at` string, required
  - `updated_at` string, required
  - `expires_at` string, nullable, required
  - `ttl_ms` integer, nullable, required
  - `kind` 'secret' | 'url' | 'login' | 'signing': Credential kind, returned when this call set opt-in fields.
  - `provider` string, nullable
  - `inject` union
    - object
      - `kind` 'header', required
      - `name` string, required
      - `format` string, required
    - object
      - `kind` 'basic', required
      - `valueAs` 'username' | 'password', required
      - `other` string
    - object
      - `kind` 'query', required
      - `param` string, required
  - `placement` object, nullable: Where placeholders may be filled. Null means anywhere. Returned when this call set opt-in fields.
    - `headers` union
      - true
      - string[]
    - `query` true
    - `path` true
    - `body` true

## Other responses

- `400`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `401`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `403`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `404`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `409`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `413`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `429`: Too many requests (edge IP/token limit) or an entitlement / plan-cap limit. Wait at least `Retry-After` seconds before retrying.
- `500`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `503`: Error envelope `{ "error": { "code", "message", "details"? } }`.
- `default`: Error envelope `{ "error": { "code", "message", "details"? } }`.

---

[API](https://skmtc.dev/kody/apis/kody-api.md) · [All operations](https://skmtc.dev/kody/apis/kody-api/llms.txt) · [OpenAPI document](https://skmtc.dev/kody/apis/kody-api/revisions/a020ecedb82b?raw)
