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

# Managing Secrets

> Store, list, revoke, and preflight hosted secrets over the API

`POST /secrets` stores a reusable hosted secret, such as a model API key or an environment variable a
task requires. This is the API equivalent of [adding secrets](/core-concepts/hosted-harbor/web-ui#adding-secrets) through the Web UI.

The examples below use `$BASE` and `$KEY`. Get a key from
[Harbor Hub API Key](/core-concepts/hosted-harbor/api-key).

```bash theme={"system"}
export BASE=https://ofhuhcpkvzjlejydnvyd.supabase.co/functions/v1
export KEY=sk-harbor-...
```

```json theme={"system"}
{
  "scope": "user",
  "env_var": "OPENAI_API_KEY",
  "value": "sk-...",
  "provider": "openai"
}
```

`scope` is `user` by default. Use `job` instead to scope a secret to a single job, which then
requires a `job_id`; `job_id` is rejected when the scope is `user`. You can pass an `org_id` to place
the secret in a specific organization, and an optional `provider` label of up to 64 characters.

The response returns metadata only, never the value:

```json theme={"system"}
{
  "id": "00000000-0000-0000-0000-000000000002",
  "scope": "user",
  "job_id": null,
  "org_id": "00000000-0000-0000-0000-00000000000a",
  "env_var": "OPENAI_API_KEY",
  "provider": "openai",
  "value_last4": "abcd",
  "status": "active",
  "created_at": "2026-08-13T14:30:00Z"
}
```

## Replacing a Secret

Storing a secret whose `env_var` already has an active secret does not silently overwrite it.
The request comes back as `409 replacement_required` along with the record you are about to replace,
so you can see exactly what is being replaced:

```json theme={"system"}
{
  "error": {
    "code": "replacement_required",
    "message": "An active secret ending in abcd already exists. Confirm that you want to supersede it.",
    "existing": {
      "id": "00000000-0000-0000-0000-000000000002",
      "scope": "user",
      "env_var": "OPENAI_API_KEY",
      "value_last4": "abcd",
      "created_at": "2026-08-13T14:30:00Z"
    }
  }
}
```

Send the same request again with `"supersede_credential_id": "<existing.id>"` to confirm. If that ID
no longer matches the active secret, because something changed in between, you get
`409 replacement_stale` instead and should re-read before retrying. Superseding
requires organization owner rights.

The same pattern applies to registry secrets below.

## Listing and Revoking Secrets

`GET /secrets` lists your secrets as metadata only. Filter with `scope`, `job_id`, and `status`,
where `status` accepts `active` (the default), `revoked`, or `all`.

```bash theme={"system"}
curl -sS "$BASE/secrets?scope=user&status=active" \
  -H "Authorization: Bearer $KEY"
```

```json theme={"system"}
{
  "secrets": [
    {
      "id": "...",
      "scope": "user",
      "job_id": null,
      "env_var": "OPENAI_API_KEY",
      "provider": "openai",
      "value_last4": "abcd",
      "status": "active",
      "created_at": "...",
      "last_used_at": null
    }
  ]
}
```

`DELETE /secrets` revokes a secret by environment variable name. Set `purge` to `true` only when the
stored record should be permanently deleted rather than marked revoked.

```json theme={"system"}
{
  "scope": "user",
  "env_var": "OPENAI_API_KEY",
  "purge": false
}
```

## Checking Secrets Before Launching

`POST /secrets/preflight` is an advisory check that reports whether each agent in a config will
actually receive a credential its model can authenticate with. It never blocks a launch on its own.

```json theme={"system"}
{
  "config": {
    "agents": [
      {
        "name": "terminus-2",
        "model_name": "openai/gpt-5-mini",
        "secrets": ["OPENAI_API_KEY"]
      }
    ]
  },
  "organization": "my-org",
  "declared_env_vars": ["OPENAI_API_KEY"]
}
```

Because secrets are scoped per agent, the check is run against each agent's own selection rather than
everything the organization stores: a name counts only when that agent's `secrets` lists it **and**
it resolves, either to an active secret in the owning organization or to a name in
`declared_env_vars`. Send `declared_env_vars` for the one-off keys you plan to pass as
`job_secrets`, since preflight never sees their values.

The response reports an overall `ok`, per-provider results under `providers`, per-agent detail under
`agent_requirements` — each with its `model`, `provider`, `missing_env_vars`, and `configured` — and
`task_requirements` listing the environment variables the resolved tasks hard-require. The older
`agents` field is still returned for existing clients. A task requirement counts as met
only when the secret is configured **and** `supplyable`. Reserved infrastructure names — `PATH`,
`LD_PRELOAD`, anything starting with `MODAL_`, `SUPABASE_`, `HOSTED_HARBOR_`, or `GCP_`, and anything
ending in `_PROXY` — are never exported into a trial's environment, so a task requiring one cannot be
satisfied by supplying your own key.
