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 through the Web UI.
The examples below use $BASE and $KEY. Get a key from
Harbor Hub API Key.
export BASE=https://ofhuhcpkvzjlejydnvyd.supabase.co/functions/v1
export KEY=sk-harbor-...{
"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:
{
"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:
{
"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.
curl -sS "$BASE/secrets?scope=user&status=active" \
-H "Authorization: Bearer $KEY"{
"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.
{
"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.
{
"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.