---
slug: agents/api-secrets
title: Secrets API
kind: reference
surface: agents
summary: REST reference for Aetherfy secrets — setting and rotating an agent's encrypted environment secrets, listing their keys, deleting them, and the workspace-scoped equivalents shared across every agent in a workspace.
sources:
  - aetherfy-control-plane:api/routes/secrets.py
  - aetherfy-control-plane:orchestrator/secrets_manager.py
  - aetherfy-control-plane:shared/error_codes.py
---

# Aetherfy secrets API

Base URL `https://agents.aetherfy.com/api/v1`, bearer authentication, and the shared
error envelope are described on the [Agent REST API index](/agents/api). `{agent}`
accepts an agent UUID or its name throughout.

## Aetherfy never returns a secret value

Every route on this page is one-way. Aetherfy encrypts a secret when you write it and
injects it into the agent's environment at run time, and there is **no endpoint that
reads a value back** — not create, not list, not rotate. The responses carry the key,
its description and its timestamps.

That is the whole security model in one sentence, and it has a practical consequence:
if you lose the plaintext, Aetherfy cannot recover it for you. Write a new value over
the same key.

## Setting an Aetherfy agent secret

`POST /api/v1/agents/{agent}/secrets` → 201

```bash
curl -s -X POST https://agents.aetherfy.com/api/v1/agents/reporter/secrets \
  -H "Authorization: Bearer $AETHERFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"OPENAI_KEY","value":"sk-…","description":"Model access"}'
```

| Field | Type | Notes |
|---|---|---|
| `key` | string | **Required.** 1–255 characters. This becomes the environment variable name |
| `value` | string | **Required.** At least 1 character. Encrypted at rest, never returned |
| `description` | string \| null | Optional, up to 512 characters |

**This route is an upsert.** Posting a key that already exists replaces its value
rather than failing — there is no separate update route, and no 409 for a duplicate
key.

```json
{
  "id": "a7c3e912-64b8-4f05-9d21-0e8b3f6c1a44",
  "agent_id": "6f1c2b7e-0a2d-4f8e-9c31-2b0d5a7e4411",
  "key": "OPENAI_KEY",
  "description": "Model access",
  "created_at": "2026-08-19T09:20:00Z",
  "updated_at": "2026-08-19T09:20:00Z"
}
```

A new or changed secret reaches the agent on its **next deploy**. Writing a secret
does not restart a running agent.

| Code | HTTP | Meaning |
|---|---|---|
| `SECRET_VALIDATION_ERROR` | 422 | The key or value was rejected — the message says why |
| `AGENT_NOT_FOUND` | 404 | |

## Listing Aetherfy agent secret keys

`GET /api/v1/agents/{agent}/secrets` → 200

```bash
curl -s https://agents.aetherfy.com/api/v1/agents/reporter/secrets \
  -H "Authorization: Bearer $AETHERFY_API_KEY"
```

```json
[
  {
    "key": "OPENAI_KEY",
    "description": "Model access",
    "created_at": "2026-08-19T09:20:00Z",
    "updated_at": "2026-08-19T09:20:00Z"
  }
]
```

Metadata only. Use this to audit which keys an agent has and when they last changed.

## Rotating an Aetherfy agent secret

`POST /api/v1/agents/{agent}/secrets/{key}/rotate` → 200

```bash
curl -s -X POST https://agents.aetherfy.com/api/v1/agents/reporter/secrets/OPENAI_KEY/rotate \
  -H "Authorization: Bearer $AETHERFY_API_KEY"
```

Rotation re-encrypts the stored value under fresh cryptographic material. **The secret
itself does not change** — the agent still sees the same value. If you want a
different value, write it with the create route instead.

| Code | HTTP | Meaning |
|---|---|---|
| `SECRET_NOT_FOUND` | 404 | No secret with that key on this agent |
| `SECRET_INTERNAL_ERROR` | 500 | Re-encryption failed. The stored secret is unchanged |

## Deleting an Aetherfy agent secret

`DELETE /api/v1/agents/{agent}/secrets/{key}` → 204, no body.

The variable disappears from the agent's environment on its next deploy. Code that
reads it will then see nothing, so remove the dependency before removing the secret.

Returns 404 `SECRET_NOT_FOUND` if there is no such key.

## Aetherfy workspace secrets

A workspace secret is available to **every agent in that workspace**, so shared
credentials are written once instead of per agent.

`POST /api/v1/workspaces/{workspace}/secrets` → 201
`GET /api/v1/workspaces/{workspace}/secrets` → 200
`DELETE /api/v1/workspaces/{workspace}/secrets/{key}` → 204

```bash
curl -s -X POST https://agents.aetherfy.com/api/v1/workspaces/research/secrets \
  -H "Authorization: Bearer $AETHERFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"SHARED_DB_URL","value":"postgres://…","description":"Team database"}'
```

The request body is identical to the agent form. The create response adds
`workspace_name` and `user_id` in place of `agent_id`; the list response is the same
key-and-metadata shape.

```json
{
  "id": "c81f2a05-9d47-4e63-b2f8-6a0c5e73d194",
  "workspace_name": "research",
  "user_id": "2f9c6b18-0a53-47e2-9d81-4b7c0e35a6f2",
  "key": "SHARED_DB_URL",
  "description": "Team database",
  "created_at": "2026-08-19T09:22:00Z",
  "updated_at": "2026-08-19T09:22:00Z"
}
```

There is no rotate route for workspace secrets — write the key again to replace it.

Deleting a workspace [deletes its secrets with it](/agents/api-workspaces), and the
delete response reports how many went.

| Code | HTTP | Meaning |
|---|---|---|
| `SECRET_VALIDATION_ERROR` | 422 | The key or value was rejected |
| `SECRET_NOT_FOUND` | 404 | No such key in this workspace |
| `WORKSPACE_NOT_FOUND` | 404 | |

## Choosing between an Aetherfy agent secret and a workspace secret

Put a credential on the workspace when more than one agent legitimately needs the same
value, and on the agent when it does not. The trade is blast radius against
duplication: a workspace secret is one thing to rotate and one thing to leak, while
per-agent copies are several of each.

See [Secrets](/agents/secrets) for the guide and the `afy` equivalents.
