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

# Credentials

> Encrypted credential vault for managing API keys, tokens, and secrets.

The credential vault is Crewship's encrypted store for API keys, OAuth tokens, and secrets. It covers the full lifecycle: CRUD on credentials, live validation (test before save and re-test stored values), zero-downtime rotation with a configurable grace window, an append-only audit timeline, and per-agent assignment. Values are protected at rest with AES-256-GCM and are never returned in API responses.

<Note>
  All credential endpoints require authentication and workspace context unless otherwise noted.
</Note>

## Endpoints

| Method        | Endpoint                                                                        | Purpose                               |
| ------------- | ------------------------------------------------------------------------------- | ------------------------------------- |
| `GET`         | [`/api/v1/credentials`](#list-credentials)                                      | List credentials with assignment info |
| `POST`        | [`/api/v1/credentials`](#create-credential)                                     | Create a credential                   |
| `GET`         | [`/api/v1/credentials/{credentialId}`](#get-credential)                         | Get credential metadata               |
| `PATCH` `PUT` | [`/api/v1/credentials/{credentialId}`](#update-credential)                      | Partial-update a credential           |
| `DELETE`      | [`/api/v1/credentials/{credentialId}`](#delete-credential)                      | Soft-delete a credential              |
| `POST`        | [`/api/v1/credentials/test`](#test-credential)                                  | Validate a value without storing it   |
| `POST`        | [`/api/v1/credentials/{credentialId}/test`](#test-stored-credential)            | Re-test a stored credential           |
| `GET`         | [`/api/v1/credentials/{credentialId}/audit`](#audit-timeline)                   | Read the credential audit timeline    |
| `POST`        | [`/api/v1/credentials/{credentialId}/rotate`](#rotate-credential)               | Rotate with grace overlap             |
| `GET`         | [`/api/v1/credentials/{credentialId}/rotations`](#list-rotations)               | List rotation history                 |
| `DELETE`      | [`/api/v1/credential-rotations/{rotationId}`](#cancel-rotation-end-grace-early) | End a grace window early              |
| `GET`         | [`/api/v1/credentials/default-env-var`](#default-environment-variable)          | Default env var name for a provider   |

***

## Managing Credentials

Core CRUD for the vault — create, read, update, and soft-delete credential records. Secret values are encrypted on write and omitted from every response.

### List Credentials

```
GET /api/v1/credentials?workspace_id={workspaceId}
```

Returns all credentials in the workspace with agent assignment info.

**Auth:** Session or CLI token + workspace membership

**Query Parameters:** (`parseListPagination` in `internal/api/credentials.go:81`)

| Parameter      | Type    | Default | Description                                                         |
| -------------- | ------- | ------- | ------------------------------------------------------------------- |
| `workspace_id` | string  | --      | Required workspace context.                                         |
| `limit`        | integer | `100`   | Page size. Capped at `500`; values `<= 0` fall back to the default. |
| `offset`       | integer | `0`     | Row offset for pagination. Negative values clamp to `0`.            |

Ordering is `type ASC, created_at DESC, id ASC`, so paging with `limit`/`offset` is stable across requests.

**Response:** `200 OK`

```json theme={null}
[
  {
    "id": "cred_abc",
    "name": "anthropic-primary",
    "description": "Main Anthropic API key",
    "type": "AI_CLI_TOKEN",
    "provider": "ANTHROPIC",
    "status": "ACTIVE",
    "scope": "WORKSPACE",
    "crew_id": null,
    "crew_ids": [],
    "account_label": "Production",
    "account_email": "team@company.com",
    "token_expires_at": "2025-12-31T00:00:00Z",
    "last_checked_at": "2024-01-15T10:00:00Z",
    "last_error": null,
    "created_at": "2024-01-01T00:00:00Z",
    "updated_at": "2024-01-15T10:00:00Z",
    "_count_agent_credentials": 5,
    "agent_names": ["Backend Dev", "Frontend Dev", "QA Engineer"],
    "mcp_used": false
  }
]
```

### Response Fields

| Field                      | Type      | Description                                                                        |
| -------------------------- | --------- | ---------------------------------------------------------------------------------- |
| `id`                       | string    | Credential ID                                                                      |
| `name`                     | string    | Display name                                                                       |
| `description`              | string?   | Description                                                                        |
| `type`                     | string    | Credential type                                                                    |
| `provider`                 | string    | Provider identifier                                                                |
| `status`                   | string    | Current status                                                                     |
| `scope`                    | string    | `WORKSPACE` or `CREW`                                                              |
| `crew_id`                  | string?   | Legacy single crew ID                                                              |
| `crew_ids`                 | string\[] | All associated crew IDs                                                            |
| `account_label`            | string?   | Account label                                                                      |
| `account_email`            | string?   | Account email                                                                      |
| `token_expires_at`         | string?   | Token expiry timestamp                                                             |
| `last_checked_at`          | string?   | Last health check timestamp                                                        |
| `last_error`               | string?   | Last error message                                                                 |
| `_count_agent_credentials` | integer   | Number of agent assignments                                                        |
| `agent_names`              | string\[] | Names of assigned agents                                                           |
| `mcp_used`                 | boolean   | Whether used by MCP bindings                                                       |
| `username`                 | string?   | USERPASS cleartext identifier (null for all other types)                           |
| `last_used_at`             | string?   | Latest USE event recorded by the audit timeline                                    |
| `last_used_ips`            | string\[] | Ring-buffer (max 5) of recent caller IPs                                           |
| `tags`                     | string\[] | Free-form tag labels (always non-null in JSON)                                     |
| `created_by_actor_type`    | string?   | Attribution (v98): `user`, `agent`, or `system`                                    |
| `created_by_actor_id`      | string?   | Attribution (v98): the actor id, `null` for `system` rows                          |
| `provisioned_for_service`  | string?   | `<crew>/<service>` tag for Crewship-managed (AUTO\_MANAGED) rows; `null` otherwise |
| `created_at`               | string    | ISO 8601 timestamp                                                                 |
| `updated_at`               | string    | ISO 8601 timestamp                                                                 |

***

## Create Credential

```
POST /api/v1/credentials?workspace_id={workspaceId}
```

**Auth:** `OWNER`, `ADMIN`, or `MANAGER` role (`canRole(role, "create")` in `internal/api/credentials_mutate.go:66`)

**Request Body:**

| Field                 | Type      | Required    | Default       | Description                                                                                                  |
| --------------------- | --------- | ----------- | ------------- | ------------------------------------------------------------------------------------------------------------ |
| `name`                | string    | Yes         | --            | Display name (1-255 characters)                                                                              |
| `value`               | string    | Conditional | --            | Secret value (required unless type is `OAUTH2`, or `pending = true`)                                         |
| `description`         | string    | No          | `null`        | Description                                                                                                  |
| `type`                | string    | No          | `"SECRET"`    | Credential type                                                                                              |
| `provider`            | string    | No          | `"NONE"`      | Provider identifier                                                                                          |
| `scope`               | string    | No          | `"WORKSPACE"` | `"WORKSPACE"` or `"CREW"`                                                                                    |
| `crew_id`             | string    | No          | `null`        | Legacy single crew ID                                                                                        |
| `crew_ids`            | string\[] | No          | `[]`          | Associated crew IDs (auto-sets scope to `CREW`)                                                              |
| `tags`                | string\[] | No          | `[]`          | Free-form tag labels                                                                                         |
| `account_label`       | string    | No          | `null`        | Account label                                                                                                |
| `account_email`       | string    | No          | `null`        | Account email                                                                                                |
| `username`            | string    | No          | `null`        | Cleartext identifier half of `USERPASS` credentials (required when type is `USERPASS`)                       |
| `token_expires_at`    | string    | No          | `null`        | Token expiry timestamp                                                                                       |
| `security_level`      | integer   | No          | `1`           | Security level (1-3)                                                                                         |
| `refresh_token`       | string    | No          | `null`        | OAuth refresh token                                                                                          |
| `oauth_client_id`     | string    | No          | `null`        | OAuth client ID (OAUTH2 type only)                                                                           |
| `oauth_client_secret` | string    | No          | `null`        | OAuth client secret                                                                                          |
| `oauth_auth_url`      | string    | No          | `null`        | OAuth authorization URL                                                                                      |
| `oauth_token_url`     | string    | No          | `null`        | OAuth token URL                                                                                              |
| `oauth_scopes`        | string    | No          | `null`        | OAuth scopes (space-separated)                                                                               |
| `pending`             | boolean   | No          | `false`       | Create a placeholder credential (status `PENDING`, no real value) used by `crewship apply -f` manifest slots |

```json theme={null}
{
  "name": "anthropic-primary",
  "type": "AI_CLI_TOKEN",
  "provider": "ANTHROPIC",
  "value": "sk-ant-api03-xxxxx",
  "scope": "WORKSPACE",
  "description": "Main Anthropic API key"
}
```

The `value` is encrypted with AES-256-GCM before storage using the format `v1:{base64(IV||AuthTag||Ciphertext)}`.

**Response:** `201 Created` -- credential object (without the encrypted value).

| Status | Condition                                                                                          |
| ------ | -------------------------------------------------------------------------------------------------- |
| `400`  | Missing name, missing value (non-OAuth2), invalid crew\_id, invalid type/USERPASS without username |
| `403`  | Insufficient role (requires MANAGER, ADMIN, or OWNER)                                              |
| `409`  | Credential with this name already exists in the workspace                                          |

***

## Get Credential

```
GET /api/v1/credentials/{credentialId}?workspace_id={workspaceId}
```

Returns credential metadata (never the encrypted value).

**Response:** `200 OK` -- credential object with `crew_ids`, `agent_names`, and `mcp_used`.

| Status | Condition            |
| ------ | -------------------- |
| `404`  | Credential not found |

***

## Update Credential

```
PATCH /api/v1/credentials/{credentialId}?workspace_id={workspaceId}
PUT /api/v1/credentials/{credentialId}?workspace_id={workspaceId}
```

Both methods behave as partial update.

**Auth:** `OWNER`, `ADMIN`, or `MANAGER` role (`canRole(role, "update")` in `internal/api/credentials_mutate.go:319`)

**Updatable Fields:**

| Field              | Type      | Description                                                                                       |
| ------------------ | --------- | ------------------------------------------------------------------------------------------------- |
| `name`             | string    | Display name                                                                                      |
| `description`      | string    | Description                                                                                       |
| `value`            | string    | New secret value (re-encrypted; resets status to ACTIVE and records an inline-rotate audit event) |
| `type`             | string    | Credential type                                                                                   |
| `provider`         | string    | Provider identifier                                                                               |
| `scope`            | string    | `WORKSPACE` or `CREW`                                                                             |
| `crew_id`          | string    | Legacy single crew ID                                                                             |
| `crew_ids`         | string\[] | Associated crew IDs (replaces existing)                                                           |
| `tags`             | string\[] | Free-form tag labels (empty array / null clears the column)                                       |
| `account_label`    | string    | Account label                                                                                     |
| `account_email`    | string    | Account email                                                                                     |
| `username`         | string    | USERPASS cleartext identifier                                                                     |
| `token_expires_at` | string    | Token expiry timestamp                                                                            |
| `security_level`   | integer   | Security level (1-3)                                                                              |

**Note:** The `status` field cannot be updated via this endpoint. Status changes are managed by the credential monitor and OAuth refresh worker.

**Response:** `200 OK` -- updated credential object.

| Status | Condition                             |
| ------ | ------------------------------------- |
| `400`  | No fields to update, invalid crew\_id |
| `403`  | Insufficient role                     |
| `404`  | Credential not found                  |

***

## Delete Credential

```
DELETE /api/v1/credentials/{credentialId}?workspace_id={workspaceId}
```

Soft-deletes the credential and clears it from all MCP bindings.

<Warning>
  Deleting a credential removes it from every MCP binding that references it.
</Warning>

**Auth:** `OWNER` or `ADMIN` role

**Response:** `200 OK`

```json theme={null}
{
  "success": true
}
```

| Status | Condition            |
| ------ | -------------------- |
| `403`  | Insufficient role    |
| `404`  | Credential not found |

***

## Validating Credentials

Probe a value against the provider's live API — either an unsaved value before storing it, or an existing stored credential.

### Test Credential

```
POST /api/v1/credentials/test
```

Validates a credential value against the provider's API without storing it. Useful for checking a key before saving. Rate-limited to 60 requests/minute per IP.

**Auth:** Session or CLI token (no workspace context needed)

**Request Body:** (`internal/api/credentials_test_endpoint.go:229`)

| Field      | Type   | Required | Description                                                   |
| ---------- | ------ | -------- | ------------------------------------------------------------- |
| `provider` | string | No       | Provider to test against (drives which API is probed)         |
| `type`     | string | No       | Credential type (used to special-case Anthropic OAuth tokens) |
| `value`    | string | Yes      | Value to test                                                 |

```json theme={null}
{
  "provider": "ANTHROPIC",
  "type": "AI_CLI_TOKEN",
  "value": "sk-ant-api03-xxxxx"
}
```

**Response:** `200 OK` with `{ "valid": bool, "status": int, "error": string }`.

***

### Test Stored Credential

```
POST /api/v1/credentials/{credentialId}/test
```

Re-tests an already-stored credential by decrypting its value server-side and probing the provider. Records an audit event so the detail-sheet timeline reflects the manual check. (`internal/api/credentials_test_endpoint.go:253`)

**Auth:** `OWNER`, `ADMIN`, or `MANAGER` role (`canRole(role, "update")`). Rate-limited to 60 requests/minute per IP.

**Response:** `200 OK` with `{ "valid": bool, "status": int, "error": string }`.

| Status | Condition            |
| ------ | -------------------- |
| `403`  | Insufficient role    |
| `404`  | Credential not found |

***

## Rotation & Audit

Rotate secrets without downtime via an overlap grace window, inspect the append-only audit trail, and manage rotation history.

### Audit Timeline

```
GET /api/v1/credentials/{credentialId}/audit?limit=50
```

Returns the credential's append-only audit timeline (`USE`, `ROTATE`, `TEST`, `REVOKE`, `DETECTED`, `CREATED` events). Backs the Audit tab in the detail Sheet. (`internal/api/credential_audit.go:276`)

**Auth:** `OWNER`, `ADMIN`, or `MANAGER` role (`canRole(role, "update")`). Audit reveals admin-action IPs, so VIEWER/MEMBER are blocked.

**Query Parameters:**

| Parameter | Type    | Default | Description                                          |
| --------- | ------- | ------- | ---------------------------------------------------- |
| `limit`   | integer | `50`    | 1-500. Out-of-range values fall back to the default. |

**Response:** `200 OK` -- JSON array (newest first).

```json theme={null}
[
  {
    "id": "ca_01h9z7k0",
    "event_type": "ROTATE",
    "agent_id": null,
    "ip_address": "10.0.4.21",
    "metadata": { "rotation_id": "rot_abc", "grace_seconds": 86400, "rotated_by": "user_5" },
    "occurred_at": "2026-05-14T09:12:44Z"
  },
  {
    "id": "ca_01h9z6jp",
    "event_type": "USE",
    "agent_id": "agent_backend",
    "ip_address": "10.0.4.21",
    "metadata": null,
    "occurred_at": "2026-05-14T09:11:02Z"
  }
]
```

| Status | Condition                                      |
| ------ | ---------------------------------------------- |
| `403`  | Insufficient role                              |
| `404`  | Credential not found (or in another workspace) |

***

### Rotate Credential

```
POST /api/v1/credentials/{credentialId}/rotate
```

Issues a new value and starts a configurable grace overlap window. The previous encrypted value is preserved on a new `credential_rotations` row for sidecar 401-fallback during the grace window, then scrubbed when `status` transitions to `EXPIRED` or `CANCELLED`. (`internal/api/credential_rotation.go:77`)

<Warning>
  Rotation replaces the live secret. The previous value is retained only for the grace window (then scrubbed), and the old value is scrubbed immediately when the rotation is expired or cancelled.
</Warning>

**Auth:** `OWNER` or `ADMIN` role (`canRole(role, "manage")`)

**Request Body:**

| Field           | Type    | Required | Default | Description                                           |
| --------------- | ------- | -------- | ------- | ----------------------------------------------------- |
| `value`         | string  | Yes      | --      | New secret value (will be encrypted with AES-256-GCM) |
| `grace_seconds` | integer | No       | `86400` | Grace window in seconds. Range `0..604800` (7 days).  |

```json theme={null}
{
  "value": "sk-ant-api03-newvalue",
  "grace_seconds": 86400
}
```

**Response:** `200 OK`

```json theme={null}
{
  "id": "rot_01h9z7k0abc",
  "credential_id": "cred_abc",
  "grace_seconds": 86400,
  "rotated_at": "2026-05-14T09:12:44Z",
  "expires_at": "2026-05-15T09:12:44Z",
  "rotated_by": "user_5",
  "status": "ACTIVE",
  "old_value_gone": false
}
```

| Status | Condition                                               |
| ------ | ------------------------------------------------------- |
| `400`  | Missing `value`, or `grace_seconds` outside `0..604800` |
| `401`  | No authenticated user on the request                    |
| `403`  | Insufficient role                                       |
| `404`  | Credential not found                                    |

***

### List Rotations

```
GET /api/v1/credentials/{credentialId}/rotations
```

Returns the rotation history for a credential, newest first. Powers the Settings tab in the detail Sheet. (`internal/api/credential_rotation.go:209`)

**Auth:** Session or CLI token + workspace membership

**Response:** `200 OK` -- JSON array.

```json theme={null}
[
  {
    "id": "rot_01h9z7k0abc",
    "credential_id": "cred_abc",
    "grace_seconds": 86400,
    "rotated_at": "2026-05-14T09:12:44Z",
    "expires_at": "2026-05-15T09:12:44Z",
    "rotated_by": "user_5",
    "status": "ACTIVE",
    "old_value_gone": false
  }
]
```

`old_value_gone` is `true` once `status` is `EXPIRED` or `CANCELLED` (the old encrypted value has been scrubbed from the row).

| Status | Condition            |
| ------ | -------------------- |
| `404`  | Credential not found |

#### Rotation Statuses

| Status      | Description                                                                                      |
| ----------- | ------------------------------------------------------------------------------------------------ |
| `ACTIVE`    | Grace window open; sidecar may fall back to `old_value` on 401                                   |
| `EXPIRED`   | Grace window elapsed; `old_value` scrubbed by the hourly expiry worker                           |
| `CANCELLED` | Operator ended grace early via `DELETE /credential-rotations/{rotationId}`; `old_value` scrubbed |

***

### Cancel Rotation (End Grace Early)

```
DELETE /api/v1/credential-rotations/{rotationId}
```

Ends an `ACTIVE` grace overlap immediately, scrubbing the stored old value. Idempotent: already-terminal rotations return `200` with the existing status. (`internal/api/credential_rotation.go:258`)

<Warning>
  Cancelling a rotation scrubs the retained old value at once, ending sidecar 401-fallback before the grace window would otherwise elapse.
</Warning>

**Auth:** `OWNER` or `ADMIN` role (`canRole(role, "manage")`)

**Response:** `200 OK`

```json theme={null}
{ "status": "CANCELLED" }
```

For an already-terminal rotation:

```json theme={null}
{ "status": "EXPIRED", "message": "rotation already terminal" }
```

| Status | Condition                                                        |
| ------ | ---------------------------------------------------------------- |
| `403`  | Insufficient role                                                |
| `404`  | Rotation not found, or its credential lives in another workspace |

***

## Lookups & Assignment

Helper lookups and the cross-reference to per-agent credential assignment.

### Default Environment Variable

```
GET /api/v1/credentials/default-env-var?provider={provider}
```

Returns the default environment variable name for a given provider (e.g., `GH_TOKEN` for `GITHUB`). The handler reads only `provider`; the `type` query parameter is not consulted (`internal/api/credentials.go:280`). Recognised providers: `GITHUB` → `GH_TOKEN`, `GITLAB` → `GITLAB_TOKEN`, `VERCEL` → `VERCEL_TOKEN`, `AWS` → `AWS_ACCESS_KEY_ID`, `KUBERNETES` → `KUBECONFIG`; any other provider returns an empty string.

**Auth:** Session or CLI token (no workspace context needed)

**Response:** `200 OK`

```json theme={null}
{ "env_var": "GH_TOKEN" }
```

***

### Agent Credential Assignment

See the [Agents](/api-reference/agents) page for credential assignment endpoints:

| Endpoint                                                     | Method | Description                |
| ------------------------------------------------------------ | ------ | -------------------------- |
| `GET /api/v1/agents/{agentId}/credentials`                   | GET    | List assigned credentials  |
| `POST /api/v1/agents/{agentId}/credentials`                  | POST   | Assign credential to agent |
| `DELETE /api/v1/agents/{agentId}/credentials/{assignmentId}` | DELETE | Remove assignment          |

***

## Reference

Enums and value sets used across the credential endpoints above.

### Credential Types

The closed type enum (`internal/api/credentials_types.go`, enforced by both Create and Update):

| Type             | Description                                                                              |
| ---------------- | ---------------------------------------------------------------------------------------- |
| `AI_CLI_TOKEN`   | LLM API key (or OAuth bearer) for agent CLI adapters                                     |
| `API_KEY`        | External service API key                                                                 |
| `CLI_TOKEN`      | Generic CLI tool token                                                                   |
| `SECRET`         | Arbitrary secret value                                                                   |
| `OAUTH2`         | OAuth 2.0 token (managed via OAuth flow)                                                 |
| `USERPASS`       | Username + password pair (`username` cleartext, password encrypted; requires `username`) |
| `SSH_KEY`        | PEM-encoded private key (validated to begin with `-----BEGIN ... PRIVATE KEY-----`)      |
| `CERTIFICATE`    | PEM-encoded certificate (validated to begin with `-----BEGIN CERTIFICATE-----`)          |
| `GENERIC_SECRET` | Opaque value, no shape validation                                                        |

### Providers

`provider` is a free-form string. The values below are the ones with a
live validation probe in the Test / Test Stored endpoints
(`internal/api/credentials_test_endpoint.go`); any other provider value
(including `NONE`) is accepted and stored, but Test returns
`valid: true` with `"No validation available for this provider"`.

| Provider       | Validation probe                                                                                  |
| -------------- | ------------------------------------------------------------------------------------------------- |
| `ANTHROPIC`    | `GET api.anthropic.com/v1/models` (AI\_CLI\_TOKEN / `sk-ant-oat` tokens accepted without probing) |
| `OPENAI`       | `GET api.openai.com/v1/models`                                                                    |
| `GOOGLE`       | `GET generativelanguage.googleapis.com/v1/models`                                                 |
| `CURSOR`       | `GET api2.cursor.sh/v0/me`                                                                        |
| `FACTORY`      | `GET app.factory.ai/api/cli/auth/whoami`                                                          |
| `GITHUB`       | `GET api.github.com/user`                                                                         |
| `GITLAB`       | `GET gitlab.com/api/v4/user`                                                                      |
| `VERCEL`       | `GET api.vercel.com/v2/user`                                                                      |
| `NONE` / other | No probe (Generic / custom)                                                                       |

### Statuses

| Status         | Description                       |
| -------------- | --------------------------------- |
| `ACTIVE`       | Ready to use                      |
| `PENDING`      | Awaiting OAuth completion         |
| `RATE_LIMITED` | Temporarily rate-limited          |
| `EXPIRED`      | Needs renewal                     |
| `REVOKED`      | Manually revoked                  |
| `ERROR`        | Validation/connection test failed |
